/******************************************************************************/
package jamie.org.sil.www.usxConverter;

import java.io.*;

import jamie.org.sil.www.usxUtils.*;
import java.util.ArrayList;
import java.util.Stack;



/******************************************************************************/
/**
 * A class to handle conversion to XHTML.
 * <p>
 * 
 * @author ARA "Jamie" Jamieson
**/

public class UsxConverterToXhtml extends UsxConverterTo
{
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                 Public                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /** General start-of-tag process, called after every end tag. */
  
  @Override public void afterUsxEndTag (String tagName,
                                        String style,
                                        int level,
                                        UsxParser.ElementDetails eltDetails,
                                        int destination) throws Exception
  {
    popDest();
    if (!"hash_text".equals(tagName)) m_ParsedTextOutputterStack.pop();
    super.afterUsxEndTag(tagName, style, level, eltDetails, destination);
  }
  
  
  /****************************************************************************/
  /** General start-of-tag process, called before every start tags. */
  
  @Override public void beforeUsxStartTag (String tagName,
                                           String style,
                                           int level,
                                           UsxParser.ElementDetails eltDetails,
                                           int destination) throws Exception
  {
    /**************************************************************************/
    super.beforeUsxStartTag(tagName, style, level, eltDetails, destination);
    
    
    
    /**************************************************************************/
    /* Make sure we use the correct stringifier. */
    
    if (!"hash_text".equals(tagName))
    {
      if ("rem".equals(style) || "restore".equals(style))
        m_ParsedTextOutputterStack.push(null); // Avoid attempting to find Bible refs in these.
      else
        m_ParsedTextOutputterStack.push(m_HrefToBibleReferenceGenerator);
    }
    
    
    
    /**************************************************************************/
    pushDest(destination);
  }

  
  /****************************************************************************/
  /**
     Runs over all input files doing processing.

     @throws Exception
   */

  @Override public void process () throws Exception
  {
    initialiseFeatures();
    initialiseGeneral();
    super.process();
  }




  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                               Protected                                **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /**
   * Generates a TOC file.
   *
   * @param outputFileName
   * @param entries List of vernacular strings and corresponding file names.
  */

  @Override protected void generateTocFile (String outputFileName, ArrayList<String[]> entries) throws Exception
  {
    /**************************************************************************/
    FileOutputStream fos = new FileOutputStream(Config.instance().getOutputFolderPath() + outputFileName);
    OutputStreamWriter osWriter = new OutputStreamWriter(fos, "UTF-8");
    BufferedWriter writer = new BufferedWriter(osWriter);



    /**************************************************************************/
    String s = Config.instance().getDcTitle();
    if (0 == s.length()) Config.instance().getTableOfContentsVernacularString();
    if (0 == s.length()) s = "Table of contents";
    ArrayList<String> text = getFileHeaderCommon(s);
    for (int i = 0; i < text.size(); ++i)
    {
      writer.write(text.get(i));
      writer.newLine();
    }



    /**************************************************************************/
    writer.write("<a class=\"");
    writer.write("toc");
    writer.write("\"></a>");
    writer.newLine();
    writer.write("<h1>");
    s = Config.instance().getTableOfContentsVernacularString();
    writer.write(0 == s.length() ? "Contents" : s);
    writer.write("</h1>");
    writer.newLine();



    /**************************************************************************/
    for (int i = 0; i < entries.size(); ++i)
    {
      String details[] = entries.get(i);
      if ("scripture".equalsIgnoreCase(details[0]))
        writeTocScripture(writer, details[1], details[2]);
      else
        writeTocPeripheral(writer, details[1], details[2], details[3]);
    }



    /**************************************************************************/
    text = getFileTrailerCommon();
    for (int i = 0; i < text.size(); ++i)
    {
      writer.write(text.get(i));
      writer.newLine();
    }



    /**************************************************************************/
    writer.close();
    osWriter.close();
    fos.close();
  }


  /****************************************************************************/
  /* Initialises the features list. */

  protected void initialiseFeatures () throws IOException
  {
    FeatureManager features = FeatureManager.instance();
    features.setOptionListForOutputOverallDisposition("Combined|MultipleFiles");
    features.setOptionListForOutputIntroductionDisposition("InSituInTextFile|None|WithFootnotes");
    features.setOptionListForOutputCrossReferencesDisposition("WithFootnotes|AtEndOfTextFile|Combined|None|PerTextFile");
    features.setOptionListForOutputEndnotesDisposition("None|WithFootnotes");
    features.setOptionListForOutputFootnotesDisposition("AtEndOfTextFile|Combined|None|PerTextFile");
    features.setOptionListForOutputScriptureTextDisposition("Combined|PerBook|PerChapter");
    features.setDefaultForOutputScriptureTextDisposition("PerBook");
    features.setAcceptDirectivesFromBookAssembly(true);
    features.setSupportsToc(true);
    features.validateConfigurationParameters(0);
  }


  /****************************************************************************/
  /* Handle the output for the end of a standard USX tag -- ie output either
     "p" or "span", along with the relevant class. */
  
  @Override protected void processUsxTagEnd (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    if ("para".equals(tagName))
    {
      if (OutputManager.C_Destination_BitBucket != destination) OutputManager.instance().removeTrailingStringFromBufferRepeated(destination, " ");
      writeLineToGivenDest(destination, "</p>");
    }
    else if ("char".equals(tagName))
      writeLineToCurrentDest("</span>");
    else
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever,
                                   ConverterException.Log.Error,
                                   "Processing for para / span end being called for " + tagName);
  }

  
  /****************************************************************************/
  /* Handle the output for the start of a standard USX tag -- ie output either
     "p" or "span", along with the relevant class. */
  
  @Override protected void processUsxTagStart (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    if (0 != level) style += level;
    if ("para".equals(tagName))
      writeToCurrentDest("<p class=\"" + style + "\">");
    else if ("char".equals(tagName))
      writeToCurrentDest("<span class=\"" + style + "\">");
    else
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever,
                                   ConverterException.Log.Error,
                                   "Processing for para / span start being called for " + tagName);
  }

  
  
  
  
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                     Specific tag handling routines                     **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Book. */
  
  @Override protected void start_book_id (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonStartOfBook(eltDetails);
    writeToCurrentDest("<a class=\"book\" id=\"b");
    writeToCurrentDest(String.format("%02d", Bookmark.instance().getBookNo()));
    writeToCurrentDest("\"></a>");
  }
  
  
  @Override protected void end_book_id (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }
  
  
  /****************************************************************************/
  /* Chapter. */
  
  @Override protected void start_chapter_c (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonStartOfChapter(eltDetails);
    
    int chapterNo = Bookmark.instance().getChapter();
    String s = "";
    
    if (1 != chapterNo && Config.instance().getIncludeBookNameInChapterHeadings()) s = Bookmark.instance().getFullVernacularName() + " ";

    writeToCurrentDest("<a class=\"chapter\" id=\"c" + Bookmark.instance().getBookNo() + "." + chapterNo + "\">");
    writeToCurrentDest("</a>");
      
    writeToCurrentDest("<p class=\"chapterNumber\">");
    writeToCurrentDest(s);
    writeToCurrentDest(NumeralProcessor.instance().toVernacular(chapterNo));
    writeToCurrentDest("</p>");
  }

  @Override protected void end_chapter_c (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }


  /****************************************************************************/
  /* Called at the start of the introductory material */

  @Override protected void start_extension_introductionBlock
    (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    linkFromScripture(OutputManager.C_Destination_Introduction,
                      BookAssembly.instance().getUltimateFileNameForIntroduction(),
                      Config.instance().getOutputIntroductionDisposition(),
		      "i",
                      "+");
    // Don't push destination here -- this is a pseudo tag.
    
    writeToGivenDest(OutputManager.C_Destination_Introduction, "<div class=\"introduction\">");
  }
  
  
  /****************************************************************************/
  /* Called at the end of the introductory material */

  @Override protected void end_extension_introductionBlock
    (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToGivenDest(OutputManager.C_Destination_Introduction, "</div>");
    // Don't pop destination here -- this is a pseudo tag.
  }
  
  
  /****************************************************************************/
  /* Footnote position in main text. */
  
  @Override protected void start_note_f (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    linkFromScripture(OutputManager.C_Destination_Footnotes,
                      BookAssembly.instance().getUltimateFileNameForFootnotes(),
                      Config.instance().getOutputFootnotesDisposition(),
		      style,
                      eltDetails.getAttribute("caller"));
    pushDest(destination);
  }

  @Override protected void end_note_f (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    popDest();
  }


  /****************************************************************************/
  /* Endnote position in main text. */
  
  @Override protected void start_note_fe (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    linkFromScripture(OutputManager.C_Destination_Endnotes,
                      BookAssembly.instance().getUltimateFileNameForEndnotes(),
                      Config.instance().getOutputEndnotesDisposition(),
		      style,
                      eltDetails.getAttribute("caller"));
    pushDest(destination);
  }

  @Override protected void end_note_fe (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    popDest();
  }


  /****************************************************************************/
  /* Remark. */
  
  @Override public void start_char_rem (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToCurrentDest("<!-- ");
  }
  
  @Override public void end_char_rem (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToCurrentDest(" -->");
  }


  /****************************************************************************/
  /* Cross-reference position in main text. */
  
  @Override protected void start_note_x (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    linkFromScripture(OutputManager.C_Destination_CrossReferences,
                      BookAssembly.instance().getUltimateFileNameForCrossReferences(),
                      Config.instance().getOutputCrossReferencesDisposition(),
		      style,
                      eltDetails.getAttribute("caller"));
    pushDest(destination);
  }

  @Override protected void end_note_x (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    popDest();
  }


  /****************************************************************************/
  /* Figure.  We don't currently do anything with figures, for copyright
     reasons. */
  
  @Override public void start_fig (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }
  
  @Override public void end_fig (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }


  /****************************************************************************/
  /* Remark. */
  
  @Override public void start_para_rem (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToCurrentDest("<!-- ");
  }
  
  @Override public void end_para_rem (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToCurrentDest(" -->");
  }


  /****************************************************************************/
  /* Headings of various descriptions. */
  
  @Override public void start_para_imt (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_imt1 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_imt2 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_imt3 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_imt4 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_is (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_is1 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_is2 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_mt (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_mt1 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_mt2 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_mt3 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_mt4 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_s (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_s1 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_s2 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }

  @Override public void start_para_s3 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonParaHeading(tagName, style, level, eltDetails, destination);
  }


  /****************************************************************************/
  /* Comment about when text was restored. */
  
  @Override public void start_para_restore (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToCurrentDest("<!-- ");
  }
  
  @Override public void end_para_restore (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToCurrentDest(" -->");
  }


  /****************************************************************************/
  /* Generated internally by the Xerces parser if it sees an XML comment. */

  @Override public void start_hash_comment (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }

  @Override public void end_hash_comment (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }


  /****************************************************************************/
  /* Generated internally by the Xerces parser to flag text. */

  @Override public void start_hash_text (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    String text = translateText(eltDetails.getNodeValue());

    
    
    /**************************************************************************/
    /* If we're processing scripture output, there's no need to worry about
     * handling cross-references, because there won't be any.  (And in fact,
     * it's best to avoid such processing anyway, because I have come across
     * one vernacular where numbers (such as 40 days and 40 nights) were
     * actually given as numerals, rather than text, which confused things.
     */
    
    if (UsxTagDetails.Canonicity.C_Canonical != m_CanonicityStack.peek())
    {
      ParsedTextOutputter outputter = m_ParsedTextOutputterStack.peek();
      if (null != outputter)
      {
        ParsedText parsedText =
          BibleReferenceParser.instance().parse(text, Bookmark.instance().getFullVernacularName(), NumeralProcessor.instance());
        text = outputter.toString(parsedText);
      }
    }
     
    writeToCurrentDest(text);
  }
  
  @Override public void end_hash_text (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }


    /**************************************************************************/
    /* User-defined stuff. */
    
    @Override protected void start_userDefinedPara_bra_introductoryMaterial (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
    {
      writeLineToGivenDest(destination, "<!-- Introductory material start -->");
    }
    
    @Override protected void end_userDefinedPara_bra_introductoryMaterial (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
    {
    }
    
    @Override protected void start_userDefinedPara_ket_introductoryMaterial (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
    {
       writeLineToGivenDest(destination, "<!-- Introductory material end -->");
    }
    
    @Override protected void end_userDefinedPara_ket_introductoryMaterial (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
    {
    }
    
    @Override protected void start_userDefinedPara_bra_ul (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
    {
      writeLineToGivenDest(destination, "<ul>");
    }
    
    @Override protected void end_userDefinedPara_bra_ul (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
    {
    }
    
    @Override protected void start_userDefinedPara_ket_ul (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
    {
      writeLineToGivenDest(destination, "</ul>");
    }
    
    @Override protected void end_userDefinedPara_ket_ul (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
    {
    }
    
    
    
  /****************************************************************************/
  /* Verse marker. */

  @Override protected void start_verse_v (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    /**************************************************************************/
    commonStartOfVerse(eltDetails);
    
    
    
    /**************************************************************************/
    /* firstVerseForWhichToOutputAnchor requires some explanation.  It was
     * introduced to handle a situation where we had 2-6a as the
     * verse number for one verse, and 6b-11 for the second.  I
     * deliberately don't include the subverse in the anchors,
     * because (rightly or wrongly) I make the assumption that if
     * anything refers to a verse, it's likely to do so at the
     * level of the verse, rather than the subverse.  However,
     * this was giving me a duplicate anchor, since both the first
     * chunk and the second chunk were making reference to 6.
     * To get round that, I check to see if the low verse from this
     * time is the same as the high from last time, in which case
     * I don't generate an anchor for the low verse.
     */
      
    int low = Bookmark.instance().getVerseLow();
    int high = Bookmark.instance().getVerseHigh();
    int firstVerseForWhichToOutputAnchor = (Bookmark.instance().getVerseLowEqualsPreviousVerseHigh() ? low + 1 : low);




    /**************************************************************************/
    for (int i = firstVerseForWhichToOutputAnchor; i <= high; ++i)
    {
      String s = "<a class=\"verse\" id=\"v";
      s += Integer.toString(Bookmark.instance().getBookNo());
      s += ".";
      s += Integer.toString(Bookmark.instance().getChapter());
      s += ".";
      s += Integer.toString(i);
      s += "\"></a>";
      s = stringRepresentingVerseAnchorTarget(s, Bookmark.instance().getBookNo(), Bookmark.instance().getChapter(), i, "");
      writeToCurrentDest(s);
    }

    if ("begin".equalsIgnoreCase(Config.instance().getVerseNumberLocation()))
      outputVerseNumber(destination,
                        Config.instance().getPrefixForVerseMarkersWhichAppearAtStartOfVerse(),
                        Config.instance().getSuffixForVerseMarkersWhichAppearAtStartOfVerse(),
                        low, Bookmark.instance().getSubVerseLow(),
                        high, Bookmark.instance().getSubVerseHigh());
  }

  @Override protected void end_verse_v (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    if ("end".equalsIgnoreCase(Config.instance().getVerseNumberLocation()))
    {
      int low = Bookmark.instance().getVerseLow();
      int high = Bookmark.instance().getVerseHigh();      
      outputVerseNumber(destination,
                        Config.instance().getPrefixForVerseMarkersWhichAppearAtStartOfVerse(),
                        Config.instance().getSuffixForVerseMarkersWhichAppearAtStartOfVerse(),
                        low, Bookmark.instance().getSubVerseLow(),
                        high, Bookmark.instance().getSubVerseHigh());
    }
  }
  
  
  /****************************************************************************/
  private void commonParaHeading (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    if (0 != level) style += level;
    // The following code would let us change the style with a view to avoiding
    // spaces between consecutive headers.  Currently Bob reckons he can do this
    // in CSS, so I've commented it out, but I've left it here in case we
    // change our minds.  Strictly speaking, with this code commented out,
    // the various callers of the present routine could all be deleted from the
    // present class, and we could just use the default behaviour implemented
    // in the parent class, but I've left stuff here so we can see how to do
    // it should we ever need to.
    //String s = m_Parser.getMostRecentParaTagDetails().split("\\:")[1];
    //if (s.matches("imt\\d?") ||
    //    s.matches("is\\d?")  ||
    //    s.matches("mt\\d?")  ||
    //    s.matches("s\\d?"))
    //  style += "_noSpaceBefore";
    writeToCurrentDest("<p class=\"" + style + "\">");
  }




  

  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                Private                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /** Carries out general initialisation. */

  private void initialiseGeneral () throws IOException, ConverterException
  {
    m_Parser = new UsxParser(this);
    m_FootnoteMarkerManager = new FootnoteMarkerManager(NumeralProcessor.instance());
    m_FootnoteMarkerManager.setPrefix("");
    m_FootnoteMarkerManager.setSuffix("");
    m_ParsedTextOutputterStack.push(m_HrefToBibleReferenceGenerator);
    
    BookAssembly.instance().setPrefixesAndSuffixes(C_NotesPrefixInTextFile,
                                                   C_NotesSuffixInTextFile,
                                                   C_StartOfMainContent,
                                                   C_EndOfMainContent);
    
    BookAssembly.instance().setHeaderAndTrailerWriters(m_HeaderAndTrailerWriterScripture,
                                                       m_HeaderAndTrailerWriterNotes, 
                                                       m_HeaderAndTrailerWriterIntroduction, 
                                                       m_HeaderAndTrailerWriterCrossReferences);
  }


  /****************************************************************************/
  /* Called at the time we hit something which requires us to link from the
     scripture file to one of the others.  Generates an anchor in the main
     file to serve as the return target, a link in the main file to the
     subsidiary file, and a return link. */

  private void linkFromScripture (int targetDestination,
                                  String targetBookName,
                                  String targetDisposition,
				  String type,
                                  String caller) throws IOException
  {
    /**************************************************************************/
    /* Nothing to do if we're generating output in situ.  Otherwise, we'll
     * need a link in the scripture file to which we can return. */
    
    if ("InSituInTextFile".equalsIgnoreCase(targetDisposition)) return;



    /**************************************************************************/
    /* Get a string we can use to identify the reference.  I include the
     * book / chapter and verse in this to make debugging easier -- it
     * doesn't have to be there to make anything work.
     */
    
    ++m_AnchorNo;
    String ref = Integer.toString(Bookmark.instance().getBookNo()) + "." +
                 Integer.toString(Bookmark.instance().getChapter()) + "." +
                 Integer.toString(0 == Bookmark.instance().getVerse() ? 1 : Bookmark.instance().getVerse()) + "." +
                 Integer.toString(m_AnchorNo);
   
    
    
    /**************************************************************************/
    /* Return target in main file. */
    
    int scripture = OutputManager.C_Destination_Scripture;
    String thisType = type + "_s2m_tgt";
    String refInMain = thisType + "_" + ref;
    writeLineToGivenDest(scripture, "");
    writeToGivenDest(scripture, "<a class=\"" + thisType + "\" id=\"" + refInMain);
    writeLineToGivenDest(scripture, "\"></a>");

    
    
    /**************************************************************************/
    /* Link main file to subsidiary file. */
    
    thisType = type + "_m2s_ref";
    String refInSubsidiary = thisType + "_" + ref;
    writeToGivenDest(scripture, "<a class=\"" + thisType + "\" href=\"");
    writeToGivenDest(scripture, targetBookName + "#" + refInSubsidiary);
    writeToGivenDest(scripture, "\">");
    writeToGivenDest(scripture, "<sup class=\"" + thisType + "\">");
    writeToGivenDest(scripture, m_FootnoteMarkerManager.getFootnoteMarker(caller));
    writeToGivenDest(scripture, "</sup>");
    writeLineToGivenDest(scripture, "</a>");

    
    
    /**************************************************************************/
    /* Anchor in subsidiary file. */
    
    thisType = type + "_s2m_tgt";
    writeLineToGivenDest(targetDestination, "");
    writeLineToGivenDest(targetDestination, "<hr />");
    writeToGivenDest(targetDestination, "<a class=\"" + thisType + "\" id=\"" + refInSubsidiary);
    writeLineToGivenDest(targetDestination, "\"></a>");

    
    
    /**************************************************************************/
    /* Link subsidiary file to main file. */
    
    thisType = type + "_s2m_ref";
    writeToGivenDest(targetDestination, "<a class=\"" + thisType + "\" href=\"");
    writeToGivenDest(targetDestination, BookAssembly.instance().getUltimateFileNameForScripture() + "#" + refInMain);
    writeToGivenDest(targetDestination, "\">");
    writeToGivenDest(targetDestination, "<sup class=\"" + thisType + "\">");
    writeToGivenDest(targetDestination, m_FootnoteMarkerManager.getMostRecentFootnoteMarker());
    writeToGivenDest(targetDestination, "</sup>");
    writeLineToGivenDest(targetDestination, "</a>");
  }

  
  /****************************************************************************/
  private final String[] C_NotesPrefixInTextFile = {"<br />", "<br />", "<br />"} ;
  private final String[] C_NotesSuffixInTextFile = null;
  private final String C_StartOfMainContent = "<!-- Start trailer block -->";
  private final String C_EndOfMainContent = "<!-- End trailer block -->";

  private int m_AnchorNo = 0;
  private Stack<ParsedTextOutputter> m_ParsedTextOutputterStack = new Stack<ParsedTextOutputter>();

  
  
  
  
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                          File output handling                          **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Used to handle scripture output. */

  private HeaderAndTrailerWriterScripture m_HeaderAndTrailerWriterScripture = new HeaderAndTrailerWriterScripture();
  private class HeaderAndTrailerWriterScripture implements OutputStreamHeaderAndTrailerWriter
  {
    @Override public void writeHeader (MyOutputStream stream) throws IOException
    {
      writeFileHeaderCommon(stream, Bookmark.instance().getFullVernacularName());
    }

    @Override public void writeTrailer (MyOutputStream stream) throws IOException
    {
      writeFileTrailerCommon(stream);
    }
  }


  /****************************************************************************/
  /* Used to handle notes output etc. */

  private HeaderAndTrailerWriterIntroNotesXref m_HeaderAndTrailerWriterCrossReferences = new HeaderAndTrailerWriterIntroNotesXref("CrossReferences");
  private HeaderAndTrailerWriterIntroNotesXref m_HeaderAndTrailerWriterIntroduction = new HeaderAndTrailerWriterIntroNotesXref("Introduction");
  private HeaderAndTrailerWriterIntroNotesXref m_HeaderAndTrailerWriterNotes = new HeaderAndTrailerWriterIntroNotesXref("Notes");
  private class HeaderAndTrailerWriterIntroNotesXref implements OutputStreamHeaderAndTrailerWriter
  {
    HeaderAndTrailerWriterIntroNotesXref (String type) { m_Type = type; }
    
    @Override public void writeHeader (MyOutputStream stream) throws IOException
    {
      String s = Config.instance().getDcTitle();
      
      String vernacularString = null;
      if (m_Type.equalsIgnoreCase("CrossReferences")) vernacularString = Config.instance().getCrossReferencesVernacularString();
      else if (m_Type.equalsIgnoreCase("Introduction")) vernacularString = Config.instance().getIntroductionVernacularString();
      else if (m_Type.equalsIgnoreCase("Notes")) vernacularString = Config.instance().getNotesVernacularString();
      
      if (0 == s.length()) s = vernacularString;
      if (0 == s.length()) s = m_Type;
      
      writeFileHeaderCommon(stream, s);
      stream.write("<a id=\"" + m_Type + "\"></a>"); stream.newLine();
      stream.write("<h1>");
      stream.write(vernacularString);
      stream.write("</h1>"); stream.newLine();
      stream.write(C_StartOfMainContent); stream.newLine();
      stream.write("<!-- " + m_Type + " -->"); stream.newLine();
    }

    @Override public void writeTrailer (MyOutputStream stream) throws IOException
    {
      stream.write(C_EndOfMainContent); stream.newLine();
      writeFileTrailerCommon(stream);
    }
    
    private String m_Type = null;
  }


  /************************************************************************/
  /**
     Common code which supplies a file header.

     @param streamName Name of Stream to which to write.

     @return Collection of lines to write out.
  */

  private ArrayList<String> getFileHeaderCommon (String title) throws FileNotFoundException, IOException
  {
    ArrayList<String> res = new ArrayList<String>();
    String s = ("RTL".equalsIgnoreCase(Config.instance().getTextDir())) ? "dir=\"rtl\" " : "";
    res.add("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
    res.add("<html " + s + "xmlns=\"http://www.w3.org/1999/xhtml\">");
    res.add("<head>");

    if ("PerChapter".equalsIgnoreCase(Config.instance().getOutputScriptureTextDisposition()))
    {
      int c = Bookmark.instance().getChapter();
      if (0 == c) c = BookAssembly.instance().getFirstAvailableChapterNumberForCurrentBook();
      
      if ("RTL".equalsIgnoreCase(Config.instance().getTextDir()))
        title = c + " " + title;
      else
        title += " " + c;
    }

    res.add("<title>" + title + "</title>");

    res.add("<!-- " + m_RunDateAndTime + " -->");

    res.add("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />");

    s = Utils.stringToHtmlString(Config.instance().getDcTitle());
    s += ", " + Config.instance().getDcLanguage() + ", Holy Bible, Scripture, Bible, Scriptures";
    res.add("<meta name=\"keywords\" content=\"" + s + "\" />");
    
    s = Utils.stringToHtmlString(Config.instance().getCopyright());
    res.add("<meta name=\"copyright\" content=\"" + s + "\" />");
    
    s = Utils.stringToHtmlString(Config.instance().getDcRights());
    res.add("<meta name=\"dc.rights\" content=\"" + s + "\" />");
    
    s = Utils.stringToHtmlString(Config.instance().getDcDescription());
    res.add("<meta name=\"description\" content=\"" + s + "\" />");
    
    s = Utils.stringToHtmlString(Config.instance().getDcPublisher());
    res.add("<meta name=\"dc.publisher\" content=\"" + s + "\" />");
    
    ArrayList<String> cssRefs = Config.instance().getCssFilePaths();
    for (String cssRef : cssRefs)
      res.add("<link rel=\"StyleSheet\" href=\"css/" + (new File(cssRef).getName()) + "\" type=\"text/css\" />");

    res.add("</head>");
    res.add("<body>");
    res.add("<div class=\"column\">");

    return res;
  }


  /************************************************************************/
  /**
     Common code which write a file trailer.

     @return Collection of lines to write out.
  */

  public ArrayList<String> getFileTrailerCommon ()
  {
    ArrayList<String> res = new ArrayList<String>();
    res.add("</div>");
    res.add("</body>");
    res.add("</html>");
    return res;
  }


  /********************************************************************/
  private void outputVerseNumber (int destination,
                                  String prefix,
                                  String suffix,
                                  int low,
                                  String lowLetter,
                                  int high,
                                  String highLetter) throws IOException
  {
    writeToGivenDest(destination, " <sup class=\"verseNumber\">");
    writeToGivenDest(destination, prefix); writeToGivenDest(destination, NumeralProcessor.instance().toVernacular(low)); writeToGivenDest(destination, lowLetter);

    if (low != high)
    {
      writeToGivenDest(destination, Config.instance().getRangeSeparator());
      writeToGivenDest(destination, NumeralProcessor.instance().toVernacular(high));
      writeToGivenDest(destination, highLetter);
    }

    writeToGivenDest(destination, suffix);
    writeToGivenDest(destination, "</sup>");
  }


  /************************************************************************/
  /**
     Common code which writes a file header.

     @param stream Stream to which to write.

     @throws IOException
  */

  private void writeFileHeaderCommon (MyOutputStream stream, String title) throws IOException
  {
    ArrayList<String> s = getFileHeaderCommon(title);
    for (int i = 0; i < s.size(); ++i)
    {
      stream.write(s.get(i));
      stream.newLine();
    }
  }


  /************************************************************************/
  /**
     Common code which write a file trailer.

     @param stream Stream to which to write.

     @throws IOException
  */

  private void writeFileTrailerCommon (MyOutputStream stream) throws IOException
  {
    ArrayList<String> s = getFileTrailerCommon();
    for (int i = 0; i < s.size(); ++i)
    {
      stream.write(s.get(i));
      stream.newLine();
    }
  }


  /****************************************************************************/
  /* Write a TOC entry for a peripheral file. */

  private void writeTocPeripheral (BufferedWriter writer,
                                   String displayName,
                                   String fileName,
                                   String anchorName) throws Exception
  {
    writer.write("<p class=\"book\"><a href=\"");
    writer.write(fileName);
    writer.write("#");
    writer.write(anchorName);
    writer.write("\">");
    writer.write(displayName);
    writer.write("</a></p>");
    writer.newLine();
  }


  /****************************************************************************/
  /* Write a TOC entry for a scripture file. */

  private void writeTocScripture (BufferedWriter writer, String vernacularFullName, String fileName) throws Exception
  {
    String ubsAbbreviation = BibleDetails.instance().getAbbreviatedUbsNameFromFileName(fileName);
    int bookNo = BibleDetails.instance().getUbsBookNumber_V(vernacularFullName);
    boolean doingPerChapterOutput = "PerChapter".equalsIgnoreCase(Config.instance().getOutputScriptureTextDisposition());

    fileName = fileName.replace("_000", ""); // Even if we're doing output per chapter, pretend
                                             // for a moment that we're not.

    int i = fileName.lastIndexOf(".");
    String firstPartOfFileName = fileName.substring(0, i);
    String secondPartOfFileName = fileName.substring(i);

    String chapterString = "";
    if (doingPerChapterOutput)
    {
      String englishName = BibleDetails.instance().getFullUbsName_V(vernacularFullName);
      chapterString = "_" + String.format("%03d", BibleDetails.instance().getFirstAvailableChapterNumberForBook(englishName));
    }

    writer.write("<p class=\"book\"><a href=\"");
    writer.write(firstPartOfFileName + chapterString + secondPartOfFileName);
    writer.write("#b");
    writer.write(String.format("%02d", bookNo));
    writer.write("\">");
    writer.write(vernacularFullName);
    writer.write("</a>");
    writer.write("<br />");
    writer.newLine();

    if ("perChapter".equalsIgnoreCase(Config.instance().getOutputTocDisposition()))
    {
      int lastChapter = BibleDetails.instance().getChapterCount_V(vernacularFullName);
      for (i = 1; i <= lastChapter; ++i)
        if (BookAssembly.instance().referenceTargetExists(ubsAbbreviation, i))
        {
          writer.write("<a href=\"");
          writer.write(firstPartOfFileName + (doingPerChapterOutput ? "_" + String.format("%03d", i) : "") + secondPartOfFileName);
          writer.write("#c");
          writer.write(Integer.toString(bookNo));
          writer.write(".");
          writer.write(Integer.toString(i));
          writer.write("\">");
          writer.write(NumeralProcessor.instance().toVernacular(i));
          writer.write("</a>");
          writer.newLine();
        }
    }

    writer.write("</p>");
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                          Specific tag handlers                         **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Generate the appropriate HREF to surround Bible references. */

  private final HrefToBibleReferenceGenerator m_HrefToBibleReferenceGenerator = new HrefToBibleReferenceGenerator();
  private class HrefToBibleReferenceGenerator extends ParsedTextOutputter
  {
    public HrefToBibleReferenceGenerator ()
    {
      setLanguage(BibleReferenceSupport.Language.C_Vernacular);
      setLength(BibleReferenceSupport.Length.C_AsSupplied);
    }
    
    @Override public String toString (ParsedTextElementBibleRef ref,
                                      BibleReferenceSupport.Length fullOrAbbreviated,
                                      BibleReferenceSupport.Language englishOrVernacular,
                                      int sepStyle,
                                      boolean includeDefaultedValues)
    {
      int chapter = ref.getChapterAsInteger(NumeralProcessor.instance());
      boolean followedByRangeSeparator = (-1 != BibleReferenceSupport.instance().isRangeSeparator(ref.getNextSeparatorCode()));
      boolean precededByRangeSeparator = (-1 != BibleReferenceSupport.instance().isRangeSeparator(ref.getPreviousSeparatorCode()));
      boolean targetExists = BookAssembly.instance().
        referenceTargetExists(BibleDetails.instance().getAbbreviatedUbsName_E(ref.getFullEnglishBookName()), chapter);
      
      boolean wantRef = Config.instance().getGenerateLinksForAllScriptureRefs();
      wantRef = wantRef && targetExists;
      wantRef = wantRef && ! ref.getFormsPartOfRangeForWhichTargetDoesNotExist();
      
      int bookNo = BibleDetails.instance().getUbsBookNumber_E(ref.getFullEnglishBookName());

      int verse = ref.getVerseAsInteger(NumeralProcessor.instance());
      if (0 == verse) verse = 1;

      String filePrefix =
        getHrefTargetWithinTextFile(bookNo,
                                    chapter,
                                    verse,
                                    ref.getSubverse());
      
      recordVerseToWhichLinkHasBeenMade(bookNo, chapter, verse, ref.getSubverse());

      String s = "";
      if (wantRef && !precededByRangeSeparator) s += "<a href=\"" + filePrefix + "\">";
      s += ref.toString(fullOrAbbreviated, englishOrVernacular, sepStyle, includeDefaultedValues);
      if (wantRef && !followedByRangeSeparator) s += "</a>";
      
      return s;
    }
  }


  /****************************************************************************/
  /* Generate an href target within the text file. */

  private String getHrefTargetWithinTextFile (int bookNo,
                                              int chapterNo,
                                              int verseNo,
                                              String subRef)
  {
    String bookName = BookAssembly.instance().getUltimateFileNameForScripture(bookNo, chapterNo);
    return bookName +
           "#v" +
           Integer.toString(bookNo) + "." +
           Integer.toString(chapterNo) + "." +
           Integer.toString(verseNo) +
           (0 == subRef.length() ? "" : "." + subRef);
  }
}
